#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <CoreLocation/CoreLocation.h>
#import <MapKit/MKGeometry.h>
#import <LockInfo/Plugin.h>

#import "FWCommon.h"
#import "FWPlugin.h"
#import "FWPreferences.h"
#import "FWPopupView.h"
#import "FWApplication.h"
#import "FWMapAnnotation.h"

#import "FWMapView.h"


static CLLocationDistance const kLatitudeBound = 50000; // 50km
static CLLocationDistance const kLongitudeBound = 50000; // 50km


@implementation FWMapView

@synthesize mapTypeBtns, locationBtn, mapView, plugin, addressLookup, lookupResponse, location, title, parsedValue, parsedXPath, parsedStatus, parsedAddress, parsedLatitude, parsedLongitude, coordinate, selectedAnnotation;


// ******************************************************************************************
#pragma mark -
#pragma mark Initialisation and object management

- (id)initWithFrame:(CGRect)rect config:(id<FWMapViewSettingsDelegate>)config plugin:(LIPlugin *)newPlugin showPopups:(BOOL)popups location:(NSString *)newLocation title:(NSString *)newTitle coordinates:(CLLocation*)coordinates
{
    if ((self = [super initWithFrame:rect])) {
        XLog(@"LI:FWMapView: Creating map view; location=%@; title=%@; coordinates=%@", newLocation, newTitle, coordinates);

        hasLocationServices = NO;
        
        self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
        showPopups = popups;
        settings = config;
        self.plugin = newPlugin;
        self.title = newTitle;
        self.location = newLocation;
        
        rect.origin.x = rect.origin.y = 0;
        rect.size.height -= 44;
        
        self.mapView = [[[MKMapView alloc] initWithFrame:rect] autorelease];
        mapView.delegate = self;
        mapView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
        
        if (coordinates) {
            XLog(@"LI:FWMapView: Passed coordinates [%@]", coordinates);
                        
            // Add an annotation for the current location
            FWMapAnnotation* annotation = [[FWMapAnnotation alloc] initWithTitle:title
                                                                        subtitle:location
                                                                   andCoordinate:coordinates.coordinate];
            [mapView performSelectorOnMainThread:@selector(addAnnotation:) withObject:annotation waitUntilDone:YES];
            [self performSelectorOnMainThread:@selector(zoomToFitMapAnnotations:) withObject:mapView waitUntilDone:YES];
            [annotation release];
            
            [self performSelector:@selector(selectAnnotation:) withObject:annotation afterDelay:0.5];
        }

        rect.origin.y = rect.size.height;
        rect.size.height = 44;
        
        UIToolbar* toolbar = [[[UIToolbar alloc] initWithFrame:rect] autorelease];
        toolbar.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin;
        toolbar.barStyle = UIBarStyleBlack;
        
        NSBundle* bundle = [NSBundle bundleForClass:[self class]];
        self.mapTypeBtns = [[[UISegmentedControl alloc]
                           initWithItems:
                           [NSArray arrayWithObjects:
                            localize(bundle, @"Map/Type/Standard"),
                            localize(bundle, @"Map/Type/Satellite"),
                            localize(bundle, @"Map/Type/Hybrid"),
                            nil]] autorelease];
        [mapTypeBtns addTarget:self action:@selector(mapTypeClicked:) forControlEvents:UIControlEventValueChanged];
        
        mapTypeBtns.segmentedControlStyle = UISegmentedControlStyleBar;
        [mapTypeBtns setWidth:80 forSegmentAtIndex:0];
        [mapTypeBtns setWidth:80 forSegmentAtIndex:1];
        [mapTypeBtns setWidth:80 forSegmentAtIndex:2];
        mapTypeBtns.selectedSegmentIndex = config.mapViewType;
        mapView.mapType = mapTypeBtns.selectedSegmentIndex;
        
        self.locationBtn = [[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction
                                                                          target:self
                                                                          action:@selector(showLocationClicked:)] autorelease];
        locationBtn.style = UIBarButtonItemStyleBordered;
        locationBtn.enabled = hasLocationServices;// shownInitialLocation && hasLocationServices;
        
        
        UIBarButtonItem *flexSpc = [[[UIBarButtonItem alloc]
                                     initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace
                                     target:nil
                                     action:nil] autorelease];
        
        [toolbar setItems:[NSArray arrayWithObjects:
                           locationBtn,
                           flexSpc,
                           [[[UIBarButtonItem alloc] initWithCustomView:mapTypeBtns] autorelease],
                           nil]];
        
        [self addSubview:toolbar];
        [self addSubview:mapView];

        // If we don't have any annotations on the map then start to lookup our location
        if (mapView.annotations.count == 0)
            [self lookupLocale];
        
    }
    
    return self;
}

- (void)dealloc
{
    NSLog(@"LI:FWMapView: Running dealloc");
    
    [mapView setDelegate:nil];
    
    [addressLookup cancel];
    
    [mapTypeBtns release];
    [locationBtn release];
    [mapView release];
    [addressLookup release];
    [lookupResponse release];
    [selectedAnnotation release];
    [title release];
    [location release];
    [plugin release];
    
    [parsedValue release];
    [parsedXPath release];
    [parsedStatus release];
    [parsedAddress release];
    [parsedLatitude release];
    [parsedLongitude release];
    
    [super dealloc];
}

- (void)willMoveToSuperview:(UIView *)newSuperview
{
    if (!newSuperview) {
        [addressLookup cancel];
        self.addressLookup = nil;

        [mapView setDelegate:nil];
    }
}


- (CLLocationDistance)degreesLatitudeForMetres:(CLLocationDistance)meters atLatitude:(CLLocationDegrees)latitude
{
    return meters / 111000;        
}

- (CLLocationDistance)degreesLongitudeForMetres:(CLLocationDistance)meters atLatitude:(CLLocationDegrees)latitude
{
    return meters / (111000 * cos(latitude));
}

- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view
{
    locationBtn.enabled = YES;
    self.selectedAnnotation = view.annotation;
}

- (void)mapView:(MKMapView *)mapView didDeselectAnnotationView:(MKAnnotationView *)view
{
    locationBtn.enabled = NO;
    self.selectedAnnotation = nil;
}

// ******************************************************************************************
#pragma mark -
#pragma mark External lookup functions

- (void)lookupAnnotationsForLocation:(CLLocationCoordinate2D)coord
{
    double boundLatitude = [self degreesLatitudeForMetres:kLatitudeBound atLatitude:coord.latitude];
    double boundLongitude = [self degreesLongitudeForMetres:kLongitudeBound atLatitude:coord.latitude];
    
    NSString* boundSearch = [NSString stringWithFormat:@"%f,%f|%f,%f",
                             coord.latitude - boundLatitude,
                             coord.longitude - boundLongitude,
                             coord.latitude + boundLatitude,
                             coord.longitude + boundLongitude];
    
    NSString* myLocation = [location stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
    
    NSString* url = [NSString stringWithFormat:@"%@?address=%@&bounds=%@&sensor=false", 
                     @"http://maps.googleapis.com/maps/api/geocode/xml",
                     [[myLocation stringByReplacingOccurrencesOfString:@" " withString:@"+"]
                      stringByAddingPercentEscapesUsingEncoding:NSASCIIStringEncoding],
                     boundSearch];
    XLog(@"LI:FWMapView: Looking up geocode location; URL = %@", url);
    
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]];
    waitingForLocation = YES;
    self.addressLookup = [[[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES] autorelease];
}

- (void)lookupLocale
{
    NSString* url = @"http://api.hostip.info/country.php";
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]];
    waitingForLocale = YES;
    self.addressLookup = [[[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES] autorelease];
}

- (void)lookupAnnotationsForLocale:(NSString*)locale
{
    NSString* myLocation = [location stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];

    NSString* url = [NSString stringWithFormat:@"%@?address=%@&region=%@&sensor=false", 
                     @"http://maps.googleapis.com/maps/api/geocode/xml",
                     [[myLocation stringByReplacingOccurrencesOfString:@" " withString:@"+"]
                      stringByAddingPercentEscapesUsingEncoding:NSASCIIStringEncoding],
                     locale];
    XLog(@"LI:FWMapView: Looking up geocode location; URL = %@", url);
    
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]];
    waitingForAnnotations = YES;
    self.addressLookup = [[[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES] autorelease];
}


// ******************************************************************************************
#pragma mark -
#pragma mark Annotation support

- (void)selectAnnotation:(FWMapAnnotation*)annotation
{
    [mapView selectAnnotation:annotation animated:YES];
}

static NSString* const kReuseIdentifier = @"FWMapViewPin";
- (MKAnnotationView *)mapView:(MKMapView *)map viewForAnnotation:(id<MKAnnotation>)annotation
{
    MKPinAnnotationView* pin = (MKPinAnnotationView*)[map dequeueReusableAnnotationViewWithIdentifier:kReuseIdentifier];
    if (!pin) {
        pin = [[[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:kReuseIdentifier] autorelease];
        pin.animatesDrop = YES;
        pin.canShowCallout = YES;
        //pin.pinColor = MKPinAnnotationColorPurple;
    }
    
    return pin;
}

- (void)removeAllAnnotations:(MKMapView*)map
{
//    [map setShowsUserLocation:NO];
    NSArray* annotations = [NSArray arrayWithArray:map.annotations];
    [map removeAnnotations:annotations];
}

static CLLocationDistance const kLatitudeSingleSpan = 1000; // 1Km
static CLLocationDistance const kLongitudeSingleSpan = 1000; // 1Km

- (void)zoomToFitMapAnnotations:(MKMapView*)map
{ 
    if (map.annotations.count == 0)
        return; 
    
    CLLocationCoordinate2D topLeftCoord = { -90, 180 }; 
    CLLocationCoordinate2D bottomRightCoord = { 90, -180 }; 
    
    for(id<MKAnnotation> annotation in map.annotations) { 
        DebugLog(@"LI:FWMapView: Adding annotation %@; lat = %f; long = %f",
                 annotation.title,
                 annotation.coordinate.latitude,
                 annotation.coordinate.longitude);
        
        topLeftCoord.latitude = fmax(topLeftCoord.latitude, annotation.coordinate.latitude); 
        topLeftCoord.longitude = fmin(topLeftCoord.longitude, annotation.coordinate.longitude); 

        bottomRightCoord.latitude = fmin(bottomRightCoord.latitude, annotation.coordinate.latitude); 
        bottomRightCoord.longitude = fmax(bottomRightCoord.longitude, annotation.coordinate.longitude); 
    } 
    
    MKCoordinateRegion region; 
    region.center.latitude = topLeftCoord.latitude - fabs(bottomRightCoord.latitude - topLeftCoord.latitude) / 2; 
    region.center.longitude = topLeftCoord.longitude + fabs(bottomRightCoord.longitude - topLeftCoord.longitude) / 2; 
    
    if (map.annotations.count == 1) {
        // Center map on the single coordinate
        region.span.latitudeDelta = [self degreesLatitudeForMetres:kLatitudeSingleSpan atLatitude:region.center.latitude];
        region.span.longitudeDelta = [self degreesLongitudeForMetres:kLongitudeSingleSpan atLatitude:region.center.latitude]; 
    }
    else {
        // Add a little extra space on the sides 
        region.span.latitudeDelta = fabs(region.center.latitude - topLeftCoord.latitude) * 2.2; 
        region.span.longitudeDelta = fabs(region.center.longitude - topLeftCoord.longitude) * 2.2; 
    }

    XLog(@"LI:FWMapView: Centering map on lat = %f; long = %f",
         region.center.latitude,
         region.center.longitude);

    // Add a little extra space on the sides 
    region = [mapView regionThatFits:region]; 
    [map setRegion:region animated:YES];
//    if ((map.centerCoordinate.latitude != region.center.latitude) || (map.centerCoordinate.longitude != region.center.longitude))
//        [map setCenterCoordinate:region.center animated:YES];
}


// ******************************************************************************************
#pragma mark -
#pragma mark HTTP location lookup

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
    self.lookupResponse = [NSMutableData dataWithCapacity:2000];
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    [lookupResponse appendData:data];
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
    self.addressLookup = nil;
    
    [self removeAllAnnotations:mapView];
    XLog(@"LI:FWMapView: Error looking up geocode location = %@", error);
    [FWPopupView displayPopupMessage:localize([NSBundle bundleForClass:[self class]], @"Map/Lookup Fail") forView:mapView];
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    self.addressLookup = nil;
    
    if (waitingForLocale) {
        waitingForLocale = NO;
        NSString* locale = [[[NSString alloc] initWithData:lookupResponse encoding:NSASCIIStringEncoding] autorelease];
        XLog(@"LI:FWMapView: Found current locale = %@", locale);
        [self lookupAnnotationsForLocale:locale];
    }
    else if (waitingForAnnotations) {
        waitingForAnnotations = NO;
        [self removeAllAnnotations:mapView];

        NSXMLParser* parser = [[NSXMLParser alloc] initWithData:lookupResponse];
        [parser setDelegate:self];
        [parser setShouldProcessNamespaces:NO]; // We don't care about namespaces
        [parser setShouldReportNamespacePrefixes:NO]; //
        [parser setShouldResolveExternalEntities:NO]; // We just want data, no other stuff
        
        [parser parse];
        [parser release];
        
        if (mapView.annotations.count > 0) {
            [self performSelectorOnMainThread:@selector(zoomToFitMapAnnotations:) withObject:mapView waitUntilDone:YES];
            
            if (mapView.annotations.count == 1)
                [self performSelector:@selector(selectAnnotation:) withObject:[mapView.annotations objectAtIndex:0] afterDelay:0.5];
        }
        else {
            [FWPopupView displayPopupMessage:localize([NSBundle bundleForClass:[self class]], @"Map/Lookup Fail") forView:mapView];
            XLog(@"LI:FWMapView: Unable to get geocode location; response = %@",
                 [[[NSString alloc] initWithData:lookupResponse encoding:NSASCIIStringEncoding] autorelease]);
        }    
    }
}


// ******************************************************************************************
#pragma mark -
#pragma mark Location tracking support

- (void)mapView:(MKMapView *)map didUpdateUserLocation:(MKUserLocation *)userLocation
{
    if (waitingForLocation) {
        waitingForLocation = NO;

        [self removeAllAnnotations:mapView];
        //[mapView setShowsUserLocation:NO];
        //[locationBtn setEnabled:hasLocationServices];
        [self lookupAnnotationsForLocation:userLocation.coordinate];

        return;
    }
    
    if (!showingLocation || shownLocation)
        return;
    
    shownLocation = YES;
    
    [self performSelectorOnMainThread:@selector(zoomToFitMapAnnotations:) withObject:mapView waitUntilDone:YES];
}


// ******************************************************************************************
#pragma mark -
#pragma mark Parsing XML

- (void)parserDidStartDocument:(NSXMLParser *)parse
{
    self.parsedXPath = [NSMutableString stringWithCapacity:100];
    self.parsedValue = [NSMutableString stringWithCapacity:100];
}

- (void)parserDidEndDocument:(NSXMLParser *)parser
{
    self.parsedXPath = nil;
    self.parsedValue = nil;
}

- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict
{
    [self.parsedValue setString:@""];
    [parsedXPath appendFormat:@"/%@", elementName];
    
    if ([elementName isEqualToString:@"result"] && [parsedXPath isEqualToString:@"/GeocodeResponse/result"]) {
        self.parsedAddress = nil;
        self.parsedLatitude = nil;
        self.parsedLongitude = nil;
    }
}

- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName
{
    if ([elementName isEqualToString:@"status"] && [parsedXPath isEqualToString:@"/GeocodeResponse/status"])
        self.parsedStatus = [NSString stringWithString:parsedValue];
    else if ([elementName isEqualToString:@"formatted_address"] && [parsedXPath isEqualToString:@"/GeocodeResponse/result/formatted_address"])
        self.parsedAddress = [NSString stringWithString:parsedValue];
    else if ([elementName isEqualToString:@"lat"] && [parsedXPath isEqualToString:@"/GeocodeResponse/result/geometry/location/lat"])
        self.parsedLatitude = [NSString stringWithString:parsedValue];
    else if ([elementName isEqualToString:@"lng"] && [parsedXPath isEqualToString:@"/GeocodeResponse/result/geometry/location/lng"])
        self.parsedLongitude = [NSString stringWithString:parsedValue];
    else if ([elementName isEqualToString:@"result"] && [parsedXPath isEqualToString:@"/GeocodeResponse/result"]) {
        if (parsedAddress && parsedLatitude && parsedLongitude) {
            CLLocationCoordinate2D coord = { parsedLatitude.doubleValue, parsedLongitude.doubleValue };
            
            // Add an annotation for the current location
            FWMapAnnotation* annotation = [[FWMapAnnotation alloc] initWithTitle:title
                                                                        subtitle:parsedAddress
                                                                   andCoordinate:coord];
            [mapView performSelectorOnMainThread:@selector(addAnnotation:) withObject:annotation waitUntilDone:YES];
            [annotation release];
            
            XLog(@"LI:FWMapView: Found new geocode location = %@", annotation);
        }
    }
    
    NSRange range = NSMakeRange(parsedXPath.length - elementName.length - 1, elementName.length + 1);
    [parsedXPath deleteCharactersInRange:range];
}

- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
{
    [parsedValue appendString:string];
}


// ******************************************************************************************
#pragma mark -
#pragma mark User input support

- (void)mapTypeClicked:(id)sender
{
    [mapView setMapType:mapTypeBtns.selectedSegmentIndex];
    settings.mapViewType = mapTypeBtns.selectedSegmentIndex;
}

- (void)showLocationClicked:(id)sender
{
    if (shownLocation || showingLocation)
        return;
    
    NSString* url = [NSString stringWithFormat:@"%@?q=%@@%f,%f&ll=%f,%f&spn=%f,%f&t=%@",
                     @"http://maps.google.com/maps",
                     [selectedAnnotation.subtitle stringByAddingPercentEscapesUsingEncoding:NSASCIIStringEncoding],
                     selectedAnnotation.coordinate.latitude, selectedAnnotation.coordinate.longitude,
                     selectedAnnotation.coordinate.latitude, selectedAnnotation.coordinate.longitude,
                     mapView.region.span.latitudeDelta, mapView.region.span.longitudeDelta,
                     mapView.mapType == MKMapTypeStandard ? @"map" : (mapView.mapType == MKMapTypeSatellite ? @"k" : @"h")];
    XLog(@"LI:FWMapView: Displaying native map; %@", url);
    
    [self.plugin launchURL:[NSURL URLWithString:url]];
}

@end
